﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal class GroupDataRangeEnumerator : DataRangeEnumeratorBase
  {
    internal GroupDataRangeEnumerator(
      IEnumerable<DataRange> ranges,
      IEnumerable<GroupDescription> groupDescriptions,
      IEnumerable<SortDescription> sortDescriptions,
      IEnumerable<SortDescription> primaryKeys,
      Type userObjectType,
      Func<string, Type> getUserObjectMemberType,
      Func<GroupHandler, SortingHandler, FilteringHandler, IAsyncEnumerator<DataPath>> enumeratorCreator )
    {
      if( enumeratorCreator == null )
        throw new ArgumentNullException( "enumeratorCreator" );

      if( userObjectType == null )
        throw new ArgumentNullException( "userObjectType" );

      if( getUserObjectMemberType == null )
        throw new ArgumentNullException( "getUserObjectMemberType" );

      // At least one range must be specified to return data from the data source.
      if( ( ranges != null ) && ( ranges.Any() ) )
      {
        // Apply the grouping configuration to the child enumerator.
        GroupHandler groupHandler = new GroupHandler();
        groupHandler.GroupCountPolicy = GroupCountPolicy.DoNotHandleCounts;
        GroupDataRangeEnumerator.AddRange( groupHandler.GroupDescriptions, groupDescriptions );

        // Apply the sorting configuration to the child enumerator.
        SortingHandler sortHandler = new SortingHandler();
        GroupDataRangeEnumerator.AddRange( sortHandler.SortDescriptions, sortDescriptions );

        // Apply a filter on the child enumerator to get only the items that are in the data ranges.
        FilteringHandler filterHandler = new FilteringHandler();
        filterHandler.FilterExpression = GroupDataRangeEnumerator.CreateFilter( ranges, primaryKeys );
        filterHandler.FilterDataItem = GroupDataRangeEnumerator.CreateFilterPredicate( ranges, primaryKeys, userObjectType, getUserObjectMemberType );

        // Create the child enumerator that will return every items inside the data ranges.
        m_enumerator = enumeratorCreator.Invoke( groupHandler, sortHandler, filterHandler );
      }
    }


    #region HasMore Property

    protected override bool HasMore
    {
      get
      {
        return ( !m_disposed )
            && ( !m_currentPosition.IsAfterLast )
            && ( m_enumerator != null );
      }
    }

    #endregion


    public override void Reset()
    {
      this.CheckState();

      // Get back at start.
      m_currentPosition = SourcePosition.BeforeFirst;
    }

    protected override void Dispose( bool disposing )
    {
      base.Dispose( disposing );

      // Release the enumerator.
      if( m_enumerator != null )
      {
        m_enumerator.Dispose();
        m_enumerator = null;
      }

      m_currentPosition = SourcePosition.AfterLast;
      m_disposed = true;
    }

    protected override IAsyncResult BeginGetNextCore( AsyncCallback callback, object state )
    {
      JumpMoveAndFetchParameters parameters = new JumpMoveAndFetchParameters( m_currentPosition, 1, 1 );

      return m_enumerator.BeginJumpMoveAndFetch(
               parameters,
               callback,
               state );
    }

    protected override GetNextResult EndGetNextCore( IAsyncResult asyncResult )
    {
      // Simulate the end of the source if the enumerator has been disposed.
      if( m_disposed )
        return new GetNextResult();

      // Set the position for the next iteration.
      JumpMoveAndFetchResult<DataPath> jumpResult = m_enumerator.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )asyncResult );
      m_currentPosition = jumpResult.EndPositionObject;

      // Retrieve the item.
      bool hasMore = this.HasMore;
      DataPath dataPath;

      if( hasMore )
      {
        dataPath = GroupDataRangeEnumerator.GetFirst( jumpResult.FetchedItems );
      }
      else
      {
        dataPath = null;
        m_currentPosition = SourcePosition.AfterLast;
      }

      return new GetNextResult( hasMore, dataPath, true );
    }

    private static FilterExpressionBase CreateFilter(
      IEnumerable<DataRange> ranges,
      IEnumerable<SortDescription> primaryKeys )
    {
      FilterExpressionBase globalFilter = null;

      foreach( DataRange range in ranges )
      {
        FilterExpressionBase filter = GroupDataRangeEnumerator.CreateRangeFilter( range, primaryKeys );

        // Add the filter to the global filter.
        if( filter != null )
        {
          if( globalFilter != null )
          {
            if( range.IsSelectionRangeInclusive )
            {
              globalFilter = new OrFilterExpression( globalFilter, filter );
            }
            else
            {
              globalFilter = new AndFilterExpression( globalFilter, new NotFilterExpression( filter ) );
            }
          }
          else
          {
            globalFilter = ( range.IsSelectionRangeInclusive ) ? filter : new NotFilterExpression( filter );
          }
        }
      }

      return globalFilter;
    }

    private static Predicate<RawDataItem> CreateFilterPredicate(
      IEnumerable<DataRange> ranges,
      IEnumerable<SortDescription> primaryKeys,
      Type userObjectType,
      Func<string, Type> getUserObjectMemberType )
    {
      Predicate<RawDataItem> predicate = null;

      foreach( DataRange range in ranges )
      {
        // Retrieve the predicate that is applied on the data range.
        Predicate<RawDataItem> dataRangeFilter = null;
        if( range.Context.Filter != null )
        {
          Predicate<object> dataRangeFilterPredicate = range.Context.Filter;
          dataRangeFilter = ( dataItem ) => dataRangeFilterPredicate.Invoke( dataItem.Data );
        }

        // Retrieve the filter expression that is applied and delimits the data range.
        Predicate<RawDataItem> dataRangeFilterExpression = null;
        FilterExpressionBase filterExpression = GroupDataRangeEnumerator.CreateRangeFilter( range, primaryKeys );
        if( filterExpression != null )
        {
          // We must create a predicate that will filter out data items since the filter expression
          // may reference a member that isn't on the user object.
          PredicateBuilder pb = new PredicateBuilder( filterExpression );
          Func<object, bool> dataItemPredicate = pb.ToPredicate<object>( userObjectType, getUserObjectMemberType );

          dataRangeFilterExpression = ( dataItem ) => dataItemPredicate.Invoke( dataItem );
        }

        // Merge the data range filters.
        Predicate<RawDataItem> dataRangePredicate;
        if( ( dataRangeFilter != null ) && ( dataRangeFilterExpression != null ) )
        {
          dataRangePredicate = ( dataItem ) => dataRangeFilter.Invoke( dataItem ) && dataRangeFilterExpression.Invoke( dataItem );
        }
        else if( dataRangeFilter != null )
        {
          dataRangePredicate = dataRangeFilter;
        }
        else
        {
          dataRangePredicate = dataRangeFilterExpression;
        }

        // Merge the data range predicate with previous data range predicate.
        if( dataRangePredicate != null )
        {
          if( predicate != null )
          {
            Predicate<RawDataItem> oldPredicate = predicate;
            predicate = ( dataItem ) => oldPredicate.Invoke( dataItem ) || dataRangePredicate.Invoke( dataItem );
          }
          else
          {
            predicate = dataRangePredicate;
          }
        }
        // The data range returns the whole the data source.
        else
        {
          return null;
        }
      }

      return predicate;
    }

    private static void AddRange<T>( ICollection<T> collection, IEnumerable<T> values )
    {
      foreach( T value in values )
      {
        collection.Add( value );
      }
    }

    private static T GetFirst<T>( IEnumerable<T> enumerable )
    {
      return ( enumerable != null )
               ? enumerable.FirstOrDefault()
               : default( T );
    }

    private static FilterExpressionBase CreateRangeFilter(
      DataRange range,
      IEnumerable<SortDescription> primaryKeys )
    {
      // Try to create a smaller filter expression to target the range.
      FilterExpressionBase filter = GroupDataRangeEnumerator.CreateNarrowRangeFilter( range, primaryKeys );
      if( filter != null )
        return filter;

      // Create a filter expression that gets the items from the starting position.
      IList<SortDescription> startSortOrder = null;
      IList<object> startValues = null;
      bool startIncludeValue = true;

      if( range.StartPosition is SortPosition )
      {
        SortPosition position = ( SortPosition )range.StartPosition;
        startSortOrder = position.SortingReference;
        startValues = position.SortValues;
        startIncludeValue = ( position.StartPosition == StartPosition.Before );
      }
      else if( range.StartPosition is ItemPosition )
      {
        ItemPosition position = ( ItemPosition )range.StartPosition;
        RawDataItem item = ( RawDataItem )( ( DataPath )position.Item ).LastChild;

        startSortOrder = range.Context.SortDescriptions;
        startValues = new List<object>();
        startIncludeValue = range.IsStartPositionInclusive;

        foreach( var sortOrder in startSortOrder )
        {
          startValues.Add( item.GetDataMemberValue( sortOrder.PropertyName ) );
        }
      }

      FilterExpressionBase startFilter = null;
      if( ( startValues != null ) && ( startValues.Count > 0 ) )
      {
        startFilter = NavigationFilterUtils.CreateFilterExpression( startSortOrder, startValues, false, startIncludeValue );
      }

      // Create a filter expression that gets the items to the end position.
      IList<SortDescription> endSortOrder = null;
      IList<object> endValues = null;
      bool endIncludeValue = true;

      if( range.EndPosition is SortPosition )
      {
        SortPosition position = ( SortPosition )range.EndPosition;
        endSortOrder = position.SortingReference;
        endValues = position.SortValues;
        endIncludeValue = ( position.StartPosition == StartPosition.After );
      }
      else if( range.EndPosition is ItemPosition )
      {
        ItemPosition position = ( ItemPosition )range.EndPosition;
        RawDataItem item = ( RawDataItem )( ( DataPath )position.Item ).LastChild;

        endSortOrder = range.Context.SortDescriptions;
        endValues = new List<object>();
        endIncludeValue = range.IsEndPositionInclusive;

        foreach( var sortOrder in endSortOrder )
        {
          endValues.Add( item.GetDataMemberValue( sortOrder.PropertyName ) );
        }
      }

      FilterExpressionBase endFilter = null;
      if( ( endValues != null ) && ( endValues.Count > 0 ) )
      {
        endFilter = NavigationFilterUtils.CreateFilterExpression( endSortOrder, endValues, true, endIncludeValue );
      }

      // Merge the start and end filters.
      if( startFilter != null && endFilter != null )
      {
        filter = new AndFilterExpression( startFilter, endFilter );
      }
      else if( startFilter != null )
      {
        filter = startFilter;
      }
      else
      {
        filter = endFilter;
      }

      // Merge the global filter of the range to the filter
      // that delimits the range.
      FilterExpressionBase contextFilter = range.Context.FilterExpression;
      if( contextFilter != null )
      {
        if( filter != null )
        {
          filter = new AndFilterExpression( contextFilter, filter );
        }
        else
        {
          filter = contextFilter;
        }
      }

      return filter;
    }

    private static FilterExpressionBase CreateNarrowRangeFilter(
      DataRange range,
      IEnumerable<SortDescription> primaryKeys )
    {
      SortPosition startPosition = range.StartPosition as SortPosition;
      SortPosition endPosition = range.EndPosition as SortPosition;

      // The range isn't delimited by SortPositions.
      if( startPosition == null || endPosition == null )
        return null;

      // The range doesn't target the same set of items.
      if( !startPosition.SortingReference.SequenceEqual( endPosition.SortingReference ) )
        return null;

      // Identify the properties that are part of the primary key.
      HashSet<string> keyPropertyName = new HashSet<string>( ( from primaryKey in primaryKeys
                                                               select primaryKey.PropertyName ).Distinct() );
      // Build a list of tuple (sort description, starting value, ending value, member of primary key).
      List<Tuple<SortDescription, object, object, bool>> rangeInfos =
        startPosition.SortingReference.Zip(
          startPosition.SortValues,
          ( sort, value ) => new Tuple<SortDescription, object, bool>( sort, value, keyPropertyName.Contains( sort.PropertyName ) ) ).Zip(
            endPosition.SortValues,
            ( info, value ) => new Tuple<SortDescription, object, object, bool>( info.Item1, info.Item2, value, info.Item3 ) ).ToList();
      // Build the list of tuple (sort description, value, member of primary key) from start
      // that share the same group level value.
      List<Tuple<SortDescription, object, bool>> sameGroupLevelRangeInfos =
        ( from rangeInfo in rangeInfos.TakeWhile( ( info ) => object.Equals( info.Item2, info.Item3 ) )
          select new Tuple<SortDescription, object, bool>( rangeInfo.Item1, rangeInfo.Item2, rangeInfo.Item4 ) ).ToList();

      // The range targets items that aren't sharing a mininum of one property value.
      if( !sameGroupLevelRangeInfos.Any() )
        return null;

      // Check out if every property that is part of the primary key has a group level value
      // and if it is shared by the starting and ending position.  If it is the case, we
      // may select the desired items using only the primary keys properties.
      bool usePrimaryKeyOnly = ( !( from rangeInfo in rangeInfos.Skip( sameGroupLevelRangeInfos.Count )
                                    where rangeInfo.Item4
                                    select rangeInfo ).Any() )
                            && ( ( from rangeInfo in sameGroupLevelRangeInfos
                                   where rangeInfo.Item3
                                   select rangeInfo.Item1.PropertyName ).Distinct().Count() == keyPropertyName.Count() );
      IEnumerable<Tuple<SortDescription, object, bool>> commonRangeInfo;

      // We only need to create a filter expression that does an equality comparison
      // on the primary key properties.
      if( usePrimaryKeyOnly )
      {
        commonRangeInfo = ( from rangeInfo in sameGroupLevelRangeInfos
                            where rangeInfo.Item3
                            select rangeInfo );
      }
      else
      {
        commonRangeInfo = sameGroupLevelRangeInfos;
      }

      // Create the part of the filter expression that uses the shared group level values.
      FilterExpressionBase filter = null;
      foreach( Tuple<SortDescription, object, bool> rangeInfo in commonRangeInfo )
      {
        FilterExpressionBase filterPart = new FilterExpression(
                                            rangeInfo.Item1.PropertyName,
                                            FilterOperator.Equal,
                                            rangeInfo.Item2 );
        if( filter != null )
        {
          filter = new AndFilterExpression( filter, filterPart );
        }
        else
        {
          filter = filterPart;
        }
      }

      // Create the part of the filter expression that doesn't share the same group level values.
      if( !usePrimaryKeyOnly )
      {
        FilterExpressionBase startFilter = NavigationFilterUtils.CreateFilterExpression( 
                                                    startPosition.SortingReference.Skip( commonRangeInfo.Count() ).ToList(),
                                                    startPosition.SortValues.Skip( commonRangeInfo.Count() ).ToList(), 
                                                    false, 
                                                    startPosition.StartPosition == StartPosition.Before );

        FilterExpressionBase endFilter = NavigationFilterUtils.CreateFilterExpression(
                                                    endPosition.SortingReference.Skip( commonRangeInfo.Count() ).ToList(),
                                                    endPosition.SortValues.Skip( commonRangeInfo.Count() ).ToList(),
                                                    true,
                                                    endPosition.StartPosition == StartPosition.After );

        if( ( startFilter != null ) && ( endFilter != null ) )
        {
          filter = new AndFilterExpression( filter, startFilter, endFilter );
        }
        else if( endFilter != null )
        {
          filter = new AndFilterExpression( filter, endFilter );
        }
        else if( startFilter != null )
        {
          filter = new AndFilterExpression( filter, startFilter );
        }
      }

      return filter;
    }


    #region Private Fields

    private IAsyncEnumerator<DataPath> m_enumerator; //null
    private SourcePosition m_currentPosition = SourcePosition.BeforeFirst;
    private bool m_disposed; //false

    #endregion
  }
}
